﻿using System;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;

namespace CzfUDP
{
	public class CzfUdpListener
	{
		public event Action<Exception> OnException;
		public event Action<CzfUDPClient> OnAccept;

		private int mtu;
		private bool started = false;

		//在内网传输，最好控制在 1472 字节 (1500-8-20) 
		//在 internet 上传输，最好控制在 548 字节 (576-8-20) 以内。
		public CzfUdpListener (Action<ThreadStart> newThread, Action<int> sleep, int mtu = 548)
		{
			this.newThread = newThread;
			this.sleep = sleep;
			this.mtu = mtu;
			this.bytes = new byte[mtu];
		}

		public void Send(byte[] bytes, EndPoint endpoint)
		{
			if (!started)
				throw new Exception ("还没有启动");

			sendCount++;
			if (sendCount >= ulong.MaxValue)
				sendCount = 0;

			if (endpoint != null)
				sock.SendTo (bytes, endpoint);
			else
				sock.Send (bytes);
		}

		Action<ThreadStart> newThread;
		Action<int> sleep;

		// 开始监听端口
		public void StartListen(int iPort){
			if (started)
				throw new Exception ("重复启动");



			started = true;
			var ipe = new IPEndPoint (IPAddress.Any, iPort);
			sock = new Socket (AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
			sock.Bind (ipe);
			BeginReceiveFrom ();
		}

		// 建立到服务器的连接
		public CzfUDPClient Connect(string host, int port, NetDeliveryMethod client_type){
			if (started)
				throw new Exception ("重复启动");
			
			started = true;
			sock = new Socket (AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
			var ipe = new IPEndPoint (IPAddress.Parse(host), port);
			sock.Connect (ipe);

			var client = new CzfUDPClient(0, mtu - 5, (byte)client_type, newThread, sleep);
			client.OnSend += ClientSend;
			BeginReceive ();

			if (client.Connect (5, 500))
				return client;
			else
				return null;
		}

		Socket sock;
		byte[] bytes;

		#region client recive
		private void BeginReceive()
		{
			sock.BeginReceive (bytes, 0, bytes.Length, SocketFlags.None, EndReceive, null);
		}

		private void EndReceive(IAsyncResult ar)
		{
			int len = 0;
			byte[] bts = null;
			try{				
				len = sock.EndReceive (ar);
				bts = CzfUDPTool.Sub(bytes, 0, len);
			}catch(Exception e){
				if (OnException != null)
					OnException (e);
				return;
			}finally{
				BeginReceive ();
			}
			if(bts != null)
				ReciveData (bts, null);
		}
		#endregion

		#region server Receive
		private void BeginReceiveFrom()
		{
			///若要保证总是返回远程主机终结点，应用程序应使用 Bind 方法显式将 Socket 绑定到本地终结点，
			/// 然后在调用 BeginReceiveFrom 方法之前，调用 SetSocketOption 方法，
			/// 并根据需要将 optionLevel 设置为 IP 或 IPv6、将 optionName 参数设置为 PacketInformation、
			/// 设置 optionValue 参数来启用此选项。否则，在接收方调用 BeginReceiveFrom 方法之前当发件人已发送一定数量的数据报时，可能不能返回到远程主机终结点。
			//			sock.SetSocketOption (SocketOptionLevel.IPv6, SocketOptionName.PacketInformation, true);
			EndPoint remote_end = new IPEndPoint(IPAddress.Any, 0);
			sock.BeginReceiveFrom(bytes,0,bytes.Length,SocketFlags.None,ref remote_end, EndReceiveFrom,null);
		}

		private void EndReceiveFrom(IAsyncResult ar)
		{
			int len = 0;
			byte[] bts = null;
			EndPoint remote_end = new IPEndPoint(IPAddress.Any, 0);
			try{				
				len = sock.EndReceiveFrom (ar, ref remote_end);
				bts = CzfUDPTool.Sub(bytes, 0, len);
			}catch(Exception e){
				if (OnException != null)
					OnException (e);
				return;
			}finally{
				BeginReceiveFrom ();
			}
			if(bts != null)
				ReciveData (bts, remote_end);
		}
		#endregion

		ulong sendCount = 0;
		ulong reciveCount = 0;
		bool debugInfo = false;


		private void ReciveData(byte[] bts, EndPoint remote_end = null)
		{
			reciveCount++;
			if (reciveCount >= ulong.MaxValue)
				reciveCount = 0;
			
			// 解析数据
			try{
				using(var ms = new BinaryReader (new MemoryStream (bts))){
					byte type = ms.ReadByte ();
//					Console.WriteLine("recive type:" + type);
					CzfUDPClient client;
					byte[] ret;
					int remote_id;
					switch (type) {
					case CzfUDPTool.CLOSE: // 收到要求断开连接包
						remote_id = ms.ReadInt32 ();
						client = CzfUDPClient.GetClientByRemoteId(remote_id);
						if(client != null)
							client.Close();
						break;
					case CzfUDPTool.SYN: // 收到要求建立联机包
						// 新建一个client
						remote_id = ms.ReadInt32 ();
						var client_type = ms.ReadByte();
						client = new CzfUDPClient(remote_id, mtu - 5, client_type, newThread, sleep);
						client.OnSend += ClientSend;
						client.remote_end = remote_end;
						// 组成响应连接包
						ret = BitConverter.GetBytes(client.id);
						// 响应建立连接
						client.Send(ret, CzfUDPTool.ACK_SYN);
						break;
					case CzfUDPTool.ACK_SYN: // 收到响应连接包
						// 找到对应的连接实例
						if(GetClient(ms, remote_end, out client))
						{
							remote_id = ms.ReadInt32 ();
							client.remote_id = remote_id;
							// 组成响应响应连接包
							ret = new byte[0];
							// 响应响应建立连接
							client.Send(ret, CzfUDPTool.ACK_ACK_SYN);
							// 连接建立成功
							client.Open();
						}
						break;
					case CzfUDPTool.ACK_ACK_SYN:
						// 找到对应的连接实例
						if(GetClient(ms, remote_end, out client))
						{
							client.Open();
							// 连接建立成功
							if(OnAccept != null)
								OnAccept(client);
						}
						break;
					default:
						// 找到对应的连接实例
						if(GetClient(ms, remote_end, out client))
						{
							// 转发到连接实例
							client.Recive(ms, type);
						}
						break;
					}
				}
			}catch(Exception e){
				// 解析错误
				if (OnException != null)
					OnException (e);
			}
		}

		// 封装头部并发送数据
		private void ClientSend(CzfUDPClient client, byte[] bytes, byte type)
		{
//			Console.WriteLine("send type:" + type);
			if (client != null) {
				// 拼上clientid
				bytes = CzfUDPTool.AddHead (bytes, client.remote_id);
				// 拼上type
				bytes = CzfUDPTool.AddHead (bytes, type);
				this.Send (bytes, client.remote_end);
			} else {
				this.Send (bytes, null);
			}
		}

		// 获取指定的client
		private bool GetClient(BinaryReader ms, EndPoint remote_end, out CzfUDPClient client)
		{
			int clientId = ms.ReadInt32 ();
//			Console.WriteLine("recive clientId:" + clientId);
			client = CzfUDPClient.GetClient (clientId);
			if (client != null)
				client.remote_end = remote_end;
			else {
				// 没有找到client,直接返回断开连接
				var bytes = BitConverter.GetBytes (clientId);
				bytes = CzfUDPTool.AddHead(bytes, CzfUDPTool.CLOSE);
				this.Send (bytes, remote_end);
			}
			return client != null;
		}
	}
}
